import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.HashSet;

/**
 * Represents a client connected to the server.
 * 
 * @author Ricky Vincent
 */
public class Client implements Runnable {
	
	private static final String END_LINE = "\n";
	
	private Socket socket;
	private DataOutputStream out;
	private BufferedReader in;
	private boolean connected = false;
	
	private Table table;
	private PlayerListener data;

	
	/**
	 * Constructs a new <code>Client</code> object with a <code>Socket</code>.
	 * 
	 * @param socket The <code>Socket</code> to bind to this <code>Client</code>.
	 * @param table The {@link Table} this client is at.
	 */
	public Client (Socket socket, Table table) {
		
		this.socket = socket;
		this.table = table;
		data = new ServerData(table);
		
		try {
			out = new DataOutputStream(socket.getOutputStream());
			in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			connected = true;
			
			//send cards initially
			sendCards();
			
		} catch (IOException e) {
			System.err.println("Could not connect to client");
		}		
	}
	
	
	public void run() {
		receiveData();
	}
	
	
	/**
	 * Receives data from the <code>Socket</code>. Blocks until data is received, or <code>disconnect</code> is called.
	 */
	private void receiveData() {
	
		String input = null;
		
		while (connected) {
			try {
				input = in.readLine();
				commandHandler(input);
				
			} catch (IOException e) {
				
				//System.err.println("Lost connection to client... retrying");
				
				try {
					//sleep for 1 second between reconnection attempts
					Thread.sleep(1000);
				} catch (InterruptedException e1) {
					continue;
				}
			}
		}
	}
	
	
	/**
	 * Handles commands. Calls the correct handler methods.
	 * 
	 * @param command the command to handle.
	 */
	private void commandHandler (String command) {
		
		if(command.charAt(0) != Commands.CMD_MOVE_BY) {
			//System.out.println("SERVER RECEIVED: " + command);
		}
	
		switch (command.charAt(0)) {
		
		case Commands.CMD_UPDATE_ALL:
			updateAll(command.substring(1));
			break;
		
		case Commands.CMD_SELECT:
			selectCards(command.substring(1));
			break;
			
		case Commands.CMD_MOVE_BY:
			moveBy(command.substring(1));
			break;
			
		case Commands.CMD_DESELECT_ALL:
			deselectAllCards();
			break;
		
		case Commands.CMD_FLIP:
			flipCards();
			break;

		case Commands.CMD_SYNC:
			try {
				sendCards();
			} catch (IOException e) {
				e.printStackTrace();
			}
			break;
			
		
		}
	}
	
	
	/**
	 * Writes bytes directly to the <code>Socket</code>.
	 * 
	 * @param data the array of bytes to write.
	 * @throws IOException
	 */
	private void sendData (byte[] data) throws IOException {
		out.write(data);
		out.flush();
	}
	
	
	/**
	 * Selects cards sent in args.
	 * 
	 * @param cards The cards to select.
	 */
	private void selectCards (String cards) {
		
		if(cards == null)
			return;
		
		String[] ids = cards.split(Commands.DEL);
		HashSet<Card> selCards = new HashSet<Card>();
		
		for (String id: ids) {
			selCards.add(table.getDeck().getCardFromID(Integer.parseInt(id)));
		}
		
		data.select(selCards);
		
		//#################HOLDS ALL SELECTED CARDS ATM FOR TESTING
		data.hold();
	}
	
	
	/**
	 * Moves held cards by the amount in args (x,y).
	 * 
	 * @param args The x & y to move cards by.
	 */
	private void moveBy (String args) {
		
		String[] xy = args.split(Commands.DEL);
		
		data.moveBy(Integer.parseInt(xy[0]), Integer.parseInt(xy[1]));
		
		//send moveBy command to all clients
		String moveArgs = args + Commands.DEL;
		
		int[] cardsToFlip = data.getHeldCardIds();
		
		for(int id: cardsToFlip) {
			moveArgs += id + Commands.DEL;
		}
		
		try {
			sendCommandToAllX(Commands.CMD_MOVE_BY, moveArgs);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	
	/**
	 * Updates all of the cards on the server. Sends updates to all connected {@link Client}'s.
	 * 
	 * @param args String containing [id#xpos#ypos#faceup(1|0)#] for each {@link Card} where # is the delimiter (<code>DEL</code>) from {@link Commands}.
	 */
	private void updateAll(String args) {
		
		String[] cards = args.split(Commands.DEL);
		int pos = 0;
		Deck deck = table.getDeck();
		Card tempCard;
		
		for(;pos < cards.length;) {
			tempCard = deck.getCardFromID(Integer.parseInt(cards[pos++]));
			tempCard.setLocation(Integer.parseInt(cards[pos++]), Integer.parseInt(cards[pos++]));
			tempCard.setFaceUp(cards[pos++].equals("1") ? true : false);
			tempCard.setZ(Integer.parseInt(cards[pos++]));
		}
		
		//Send update to all clients
		String cardsString = "";
		
		for(Card card: table.getDeck().getDeck()) {
			cardsString += card.getImmutableID() + Commands.DEL + card.getLocation().x + Commands.DEL + card.getLocation().y + Commands.DEL + (card.isFaceUp() ? 1 : 0) + Commands.DEL  + card.getZ() + Commands.DEL;
		}
		
		try {
			sendCommandToAllX(Commands.CMD_UPDATE_ALL, cardsString);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	
	/**
	 * Deselects all held cards.
	 */
	private void deselectAllCards() {
		data.release();
	}
	
	
	/**
	 * Flips the held cards. Notifies all clients of the flips.
	 */
	private void flipCards() {
		data.flip();
		
		//send flip to all clients
		String flipArgs = "";
		int[] cardsToFlip = data.getHeldCardIds();
		
		for(int id: cardsToFlip) {
			flipArgs += id + Commands.DEL;
		}
		
		try {
			sendCommandToAllX(Commands.CMD_FLIP, flipArgs);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	
	/**
	 * Sends a command to this <code>Client</code>
	 * 
	 * @param command The command to send from {@link Commands}.
	 * @param args The arguments to send with the command.
	 * @throws IOException
	 */
	public void sendCommand(char command, String args) throws IOException {
		args = command + args + END_LINE;
		sendData(args.getBytes());
	}
	
	
	/**
	 * Sends a command to all connected clients apart from this one.
	 * @throws IOException 
	 */
	public void sendCommandToAllX (char command, String args) throws IOException {
		
		for(Client client: CardsServer.clients) {
			
			if(client != this) {
				client.sendCommand(command, args);
			}
		}
	}

	
	/**
	 * @return the <code>Socket</code>
	 */
	public Socket getSocket() {
		return socket;
	}
	
	
	/**
	 * Disconnects this client.
	 */
	public void disconnect() {
		connected = false;
	}


	/**
	 * @return the table
	 */
	public Table getTable() {
		return table;
	}


	/**
	 * @param table the table to set
	 */
	public void setTable(Table table) {
		this.table = table;
	}
	
	
	/**
	 * Sends all of the cards to this <code>Client</code>.
	 * @throws IOException
	 */
	public void sendCards() throws IOException {
		String cardsString = "";
		
		for(Card card: table.getDeck().getDeck()) {
			cardsString += card.getImmutableID() + Commands.DEL + card.getLocation().x + Commands.DEL + card.getLocation().y + Commands.DEL + (card.isFaceUp() ? 1 : 0) + Commands.DEL + card.getZ() + Commands.DEL;;;
		}

		sendCommand(Commands.CMD_UPDATE_ALL, cardsString);
		//System.out.println(cardsString);
	}
}
